![AWS::Includeを利用してLambda-backed Custom Resourceをモジュール化する](https://devio2023-media.developers.io/wp-content/uploads/2014/05/AWS_CloudFormation.png)
AWS::Includeを利用してLambda-backed Custom Resourceをモジュール化する
はじめに
こんにちは、中山です。
以前、最近発表されたAWS::Includeについて以下のエントリでご紹介しました。
個人的に嬉しいアップデートの1つだったので早速使っています。いろいろと触っているのですが、この機能を利用することでLambda-backed Custom Resourceのモジュール化ができそうです。この場合の「モジュール化」とは、プログラミング言語のそれと同等の意味です。つまり、大抵の言語では import
や require
などで別のプログラムを呼び出して利用することができますが、CloudFromationでも同等のことが可能になりました。以前までもネストスタックやクロススタック参照を利用することで、同じようなことはできたのですが、よりプログラミング言語的な意味で「モジュール化」できるようになったなと感じています。けっこう便利だと思うので、本エントリでご紹介したいと思います。
Lambda-backed Custom Resourceと組み合わせると嬉しい理由
CloudFromationを使っていると、Lambda-backed Custom Resourceは必須のものになっていると思います。CloudFromationは組み込み関数が少ないので、動的な処理(数値計算やAMI IDの検索など)をテンプレートで実施する必要がある場合、必ずこの機能のお世話になります。
とても便利な機能なのですが、個人的には以下の問題点があると思っています。
- テンプレートで定義する必要のあるリソースが多い
- インラインでLambda関数を定義している場合に別のテンプレートで再利用しにくい
それぞれ以下で解説します。
テンプレートで定義する必要のあるリソースが多い
Lambda-backed Custom Resourceを利用するためには最低限以下の3リソースを定義する必要があります。
- Lambda関数本体を定義する
AWS::Lambda::Function
リソース - Lambda関数用のIAM Roleを定義する
AWS::IAM::Role
リソース - Lambda関数の結果をテンプレートで扱うためのカスタムリソース
例えば、パラメータで渡された数値をもとに計算する処理が必要だったとします。本来は数値計算だけをやりたいにも関わらず、こういったリソースを定義しなければならないとするとちょっと大げさな印象があります。私はTerraformもよく利用するのですが、こちらの場合データソースや組み込み関数を利用することでほぼ同等のことができます。内部的にLambda関数は当然使われてないので、IAM Roleを作る必要もありません。こういった事情もあり、ちょっと面倒くさいなと思ってました。
インラインでLambda関数を定義している場合に別のテンプレートで再利用しにくい
Lambda-backed Custom Resourceで定義するLambda関数のコードは以下の2つの方法で利用可能です。
- S3にアップロードされたコードを参照する
- インラインでテンプレート内に記載する
S3にアップロードしておけば別のテンプレートから参照できるため、その点は便利です。しかし、インラインの場合cfn-responseモジュールを使えるので、個人的にはインラインの方法をよく利用しています。ただ、テンプレートにインラインで定義してしまうと再利用性が低くなる(テンプレートにハードコードする必要がある)ので、悩みどころのある選択でした。
AWS::Includeとの組み合わせ
こういった問題点があったのですが、 AWS::Include
を利用することで解決できそうです。今回は最新のAMI IDを検索するLambda-backed Custom Resourceを使って、どんな感じで利用するのかご紹介してみます。
呼び出し元( AWS::Include
を利用する方)のテンプレートは以下のような感じです。
--- AWSTemplateFormatVersion: 2010-09-09 Description: Lambda-backed Custom Resource Demo Parameters: ArtifactBucket: Type: String Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: General Configuration Parameters: - ArtifactBucket ParameterLabels: ArtifactBucket: default: Artifact Bucket Resources: Fn::Transform: Name: AWS::Include Parameters: Location: !Sub s3://${ArtifactBucket}/latest-ami.yml Outputs: ImageId: Value: !GetAtt CustomAMIInfo.ImageId
Resources
セクションの下に AWS::Include
で別のテンプレートをIncludeしています( latest-ami.yml
という名前でS3に保存している)。
Outputs
セクションに注目してください。このテンプレート内では定義していない、 CustomAMIInfo
という論理IDを !GetAtt
関数で参照しています。この論理IDはIncludeしたテンプレート内で定義しているのですが、同じテンプレートで定義されているかのように参照可能です。
Lambda-backed Custom Resourceを利用しているテンプレートは以下のような感じです。
LambdaRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Sid: AssumeRolePolicy Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: EC2Access PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - ec2:DescribeImages Resource: "*" LambdaAMIInfo: Type: AWS::Lambda::Function Properties: Handler: index.handler Role: Fn::GetAtt: LambdaRole.Arn Runtime: python2.7 Timeout: 20 Code: ZipFile: | from __future__ import print_function from botocore.exceptions import ClientError import cfnresponse import boto3 def handler(event, context): if event['RequestType'] == 'Delete': cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) response_data = {} filters = [ {'Name': 'architecture', 'Values': ['x86_64']}, {'Name': 'root-device-type', 'Values': ['ebs']}, {'Name': 'name', 'Values': ['amzn-ami-hvm-*']}, {'Name': 'virtualization-type', 'Values': ['hvm']}, {'Name': 'block-device-mapping.volume-type', 'Values': ['gp2']}] try: images = boto3.client('ec2').describe_images(Owners=['amazon'], Filters=filters) except ClientError as e: print(e['Error']['Message']) cfnresponse.send(event, context, cfnresponse.FAILED, {}) for i in sorted([image for image in images['Images']], key=lambda x: x['Name']): if i['Name'].lower().count('beta') > 0 or i['Name'].lower().count('.rc') > 0: continue response_data['ImageId'] = i['ImageId'] cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) CustomAMIInfo: Type: Custom::AMIInfo Version: 1.0 Properties: ServiceToken: Fn::GetAtt: LambdaAMIInfo.Arn
テンプレートで定義しているリソース自体は特に変わったところは無いのですが、リソースが Resources
セクション内に定義されていない点に注目してください。このテンプレートは先程のテンプレートで呼び出されます。呼び出し元で Resources
を定義しているため、こちらのテンプレートでは不要になるというわけです。
あと、少しハマったのですが AWS::IAM::Role
リソースの Version
プロパティに渡す値は '
か "
で囲まないと以下のようなエラーになりました。
The policy must contain a valid version string
AWS::Include
を利用していないテンプレートだと特にエラーは出力されないので、内部的に別のバリデーションが実施されているのかもしれません。
動作確認
テンプレートをいつも通り作成したら、結果を確認してみます。
- 3つのリソースが作成されていることを確認
$ aws cloudformation list-stack-resources \ --stack-name <_YOUR_STACK_NAME_> \ --query 'StackResourceSummaries[].[ResourceStatus,LogicalResourceId]' \ --output text CREATE_COMPLETE CustomAMIInfo CREATE_COMPLETE LambdaAMIInfo CREATE_COMPLETE LambdaRole
- アウトプットにAMI IDが表示されることを確認
$ aws cloudformation describe-stacks \ --stack-name <_YOUR_STACK_NAME_> \ --query 'Stacks[].Outputs' \ --output text ImageId ami-56d4ad31
まとめ
いかがだったでしょうか。
AWS::Include
をLambda-backed Custom Resourceと組み合わせてモジュール化する方法をご紹介しました。これで再利用性のあるテンプレートの開発ができそうですね。
ただ、個人的にはGitHubにアップロードしたテンプレートをIncludeできる機能がほしいなと思ってます。広く一般に公開するテンプレートを作成する場合、S3に保存する必要があるとどのバケットでホストするのか、誰がコスト負担するのかなどを考慮する必要があるからです。AWSさん検討お願いします!!!!1111
本エントリがみなさんの参考になれば幸いに思います。